[Previous] [Next]

Creating an ActiveX EXE Server

If you have a Visual Basic program that's already structured in classes, converting it to an ActiveX server requires just a few mouse clicks. As you'll see in a moment, you don't even need to compile the application into an actual EXE file to test the component, and you can debug it inside the Visual Basic IDE using all the tools that the environment gives you.

Of course, you can also start an ActiveX component from scratch by issuing the New Project command from the File menu and then selecting the ActiveX EXE item from the project gallery. In this situation, Visual Basic creates a project that contains one Public class module instead of a form.

The Basic Steps

It's customary, when showing how to implement a new technology, to start with a simple example. In this case, however, we can recycle one of the class-based samples that we developed in Chapter 7, the CFileOp application.

Setting the project properties

The first thing to do is unload all the modules that aren't really necessary. When you're transforming the CFileOp application into an ActiveX server, you don't need the Form1 form any longer, so you can remove it from the project. Don't delete it from the disk, however, because you'll need it again soon.

The next step is to turn this project into an ActiveX EXE application, which you do from within the General tab of the Project Properties dialog box. (See Figure 163.) You should also give a meaningful name to the project—for example, FileOpSvr. This becomes the name of the library that client programs have to reference to use the objects exposed by this application. Select (none) in the Startup Object field, and add a description for the project—in this case, "A component for file operations." This description will appear in the References dialog box of client programs.

Finally, go to the Component tab of the dialog box and make sure that the StartMode setting is ActiveX Component. This setting tells the Visual Basic environment that you want to test the current project as if it were invoked as a component from another application. Don't forget that ActiveX EXE applications can also be run as regular Windows applications; to test how they behave in that case, set the StartMode option to Standalone.

Figure 16-3. The Project Properties dialog box with all the properties set to create the FileOpSvr server application.

Setting class properties

The FileOpSvr project is almost ready to run, but Visual Basic will refuse to actually execute it until it contains at least one Public creatable class. Because you converted a Standard EXE project, the Instancing property of the CFileOp class module is set to 1-Private, and private classes aren't visible to the outside. To comply with Visual Basic requirements, you must change this property to 5-MultiUse, which means that the class is Public and its instances can be created from client applications. (You need to know more about the Instancing property, and you will learn in the next section.)

Running the server project

At this point, you're ready to run the server application. If you press F5, however, a dialog box appears; this is the Debugging page of the Project Properties dialog box. Ensure that the "Wait For Components To Be Created" option is selected, and then click on the OK button to start the server. If all the settings are correct, you'll see that the program is running, but nothing else happens. This is the normal behavior: Visual Basic is waiting for a client application to request an object from this component.

TIP
When running an ActiveX EXE or DLL project, you should deselect the Compile On Demand option in the General tab of the Options dialog box. This setting ensures that no compilation or syntax errors can occur while the program is providing its objects to client applications, which in most cases would force you to stop both the client and the server applications, fix the error, and restart. If you don't want to modify this IDE setting, you can force a full compilation by pressing the Ctrl+F5 combination instead of the F5 key or by issuing the Run With Full Compile command from the Run menu.

Creating the client application

It's time to recycle the Form1 form that you discarded from the ActiveX EXE project. Launch another instance of the Visual Basic environment, select a Standard EXE project type, if necessary, and then remove the Form1 module that Visual Basic automatically creates. You must do this to prevent name conflicts.

At this point, you can issue an Add File command from the Project menu to add the CFileOp.Frm file to the project. (You can use the Ctrl+D key shortcut.) You need to make this form the Startup Object for this project, which you do from within the General tab of the Project Properties dialog box. If you now run the client project, you'll get a compiler error ("User-defined type not defined"), caused by the following line in the declaration section of the Form1 module:

Dim WithEvents Fop As CFileOp

The reason should be evident: The CFileOp object is now external to the current application, and for Visual Basic to find it you must add a reference to it in the References dialog box of the application. Selecting the FileOpSvr project is simple because its description, "A component for file operations," appears early in the alphabetical list of all the components registered on the system. If you're in doubt, however, just check the Location field near the bottom of the dialog box. This string should point to the VBP project file or, if you haven't saved the project yet, to a temporary file in the Windows TEMP directory. Checking this value might be necessary when you search among multiple components with the same description, as often occurs when there are different versions of the same component.

Testing the client

After you add a reference to the FileOpSvr project, you can finally run the client application and see that it behaves like the original class-based program. The invisible difference, however, is that all the objects are external to the application and communicate with the application through COM. What's even more exciting is that you can debug this COM-based application as if it were a standard Visual Basic project. In fact, you can trace any cross-application call using the F8 function key, and you'll be transported from the client project's source code into the server's source code and back. This apparently minor feature is actually a great lifesaver, which can save you hours when testing your ActiveX clients and servers.

When you're finished with the testing phase, you should close the client application's form and then stop the server application by clicking on the End button on the toolbar. (This is one of the few circumstances when it's OK to stop a running application using the End button.) If you try to perform these actions in the reverse order, a warning appears when you try to stop the server, as you can see in Figure 16-4. If you click on the Yes button to confirm the termination of the server application, the client program raises an error when it tries to use the object pointed to by the Fop variable.

Click to view at full size.

Figure 16-4. You shouldn't end a server application if a client is currently using its objects.

The Instancing Property

The Instancing property of a class module determines how objects of that class can be created and referenced from client applications via COM. This property can be assigned six different values, even though not all of them are available within the four types of projects, listed in Table 16-1, that you can build with Visual Basic.

Table 16-1. The available values for the Instancing property in different types of projects.

Standard EXE ActiveX EXE ActiveX DLL ActiveX Control
1-Private x x x x
2-PublicNotCreatable x x x
3-SingleUse x
4-Global SingleUse x
5-MultiUse x x
6-Global MultiUse x x

Selecting the most appropriate setting

You need to understand the differences among the possible settings of the Instancing property. At runtime, you can neither read nor modify the values of the properties of a class listed in the Properties window. Unlike properties of controls, properties of classes are design-time_only properties. The possible settings of the Instancing property are listed below.

Private Private class modules aren't visible outside the current project. In other words, not only can't client applications create classes of this type, they can't even reference these objects. In fact, the server application isn't allowed to pass an instance of a Private class to its client (for example, as a return value of a function or through an argument of a procedure). All class modules in Standard EXE projects are Private, and for this reason the Instancing property isn't available in those projects.

PublicNotCreatable These classes are visible from outside the project, but client applications can't directly create their instances. It means that clients can declare variables of their types and can assign these references using the Set command but can't use the New keyword or the CreateObject function to create instances of these classes. The only way for a client to get a valid reference to a PublicNotCreatable class is by asking the server to return it—for example, through a method of another class. For this reason, Visual Basic requires that all ActiveX projects contain at least one creatable class.

SingleUse SingleUse objects are public and creatable, so clients can both declare variables of their type and create the instances using New or CreateObject. When a new object is created by the client, COM loads a new instance of the server, each time in a different address space. For example, if a client application creates 10 SingleUse objects, COM runs 10 different processes, each one providing one instance of the object.

MultiUse MultiUse objects are public and creatable, but unlike SingleUse objects one single instance of the component provides all the objects requested by client applications. This is the default setting for class modules added to an ActiveX EXE or ActiveX DLL project, and is also the most reasonable setting in most cases.

GlobalSingleUse and GlobalMultiUse These are variants of the SingleUse and MultiUse settings, respectively. Global objects are described later in this chapter.

Private and Public objects

The most important feature of a class is its scope. If the Instancing property is 1Private, none of the instances of the class can be seen from outside the server. In all other cases, these objects can be manipulated by client applications and can be freely passed from the server to the client and vice versa—for example, as arguments to methods or as the return value of properties and functions.

If a client application were able to get a reference to a private object of the server, a series of nasty things might happen, including fatal errors or even GPF errors. Fortunately, you don't run any serious risk because the Visual Basic compiler prohibits the server from returning a Private object to its clients. For example, if your server component defines a Private class, and you create a Public class with a Public method that returns an instance of the Private class, the Visual Basic compiler raises the error message shown in Figure 16-5. The same thing happens when you attempt to pass clients a UDT defined in a BAS module of the component because everything defined in a BAS module is considered to be Private to the component, even if it's declared with the Public keyword.

Click to view at full size.

Figure 16-5. It isn't legal for a server to pass Private objects and data structures to its clients.

MultiUse and SingleUse objects

To understand the differences between SingleUse and MultiUse objects, you must keep in mind that Visual Basic creates single-threaded components unless you explicitly request that it build a multithreaded component. (Multithreading is covered in the section "Multithreaded ActiveX Components" later in this chapter.)

A single-threaded MultiUse component can serve only one client at a time; in other words, even if the component provides many objects to its clients, only one object can execute code in a given moment. So if two clients each request an object from the component and then execute a method at the same time, only one of the clients will be served immediately, and the other has to wait until the method in the first object completes its execution. No request is lost, however, because COM automatically serializes all clients' requests; requests are postponed and placed in a queue. Each request will remain in the queue until the server has completed all the tasks that were queued before it.

This one-thing-at-a-time limitation has been addressed by multithreaded MultiUse components, which can create multiple threads of execution, where each thread can provide a different object. Multithreaded components can therefore serve more clients without one client blocking the activity of other clients.

Conversely, each SingleUse object is provided by a different process. The main advantage of SingleUse objects is that they can multitask. In other words, each client can instantiate an object in a different process, and it never has to compete for the component with other clients. On the other hand, because each individual instance of a SingleUse class runs in a separate process, SingleUse objects require more memory and system resources than MultiUse objects. By and large, you can assume that each additional instance of a SingleUse object takes about 800 KB of memory, so it's clear that you can't use SingleUse objects when you envision the creation of hundreds or thousands of objects. In practice, you can't run more than one or two dozen SingleUse objects even on a high-end system. This is so because when too many processes are running, your CPU spends more time switching from one process to the other than actually executing code in the processes themselves.

Another problem with SingleUse components is that you can't completely test them inside the Visual Basic environment. The IDE can provide only one SingleUse object, and when the client requests a second object the Visual Basic instance that's providing the SingleUse component displays a warning message. A few seconds after the warning, the client application receives an error 429, "ActiveX component can't create object." To fully test a SingleUse component, you must compile it to an EXE file and have your clients reference this EXE file instead of the component provided by the Visual Basic environment.

All things considered, your best choice usually is to create single-threaded or multithreaded MultiUse objects. This is also the more scalable solution, in the sense that you can provide 10, 100, or even 10,000 objects without consuming all the memory and the CPU time of your system. You have no choice when working with in-process ActiveX. Because an ActiveX DLLs runs in the address space of its client, it isn't possible to create multiple instances of the component in different address spaces. For this reason, ActiveX DLL projects don't support the SingleUse attribute.

Whatever your decision is, the most important point is that you should never mix MultiUse and SingleUse objects (or their Global variants) in the same ActiveX EXE server. If you do, you have no control over which particular instance of the component is providing MultiUse objects and a given client could have its objects supplied by different instances, which is usually something that you should avoid.

In practice, if a SingleUse component exposes a hierarchy of objects, you make the root of the hierarchy the only creatable object and you make all the other Public objects in the hierarchy PublicNotCreatable. You must also provide your client with a number of constructor methods to have the server create an instance of each of such dependent objects. For more information about object hierarchies and constructor methods, see Chapter 7.

Internal instancing

An ActiveX server can instantiate an object defined in its own Visual Basic project. In this situation, the rules that affect how the object is created and used are slightly different:

Given all the COM overhead, it shouldn't be surprising that using CreateObject to instantiate a Public object defined in the same project is 4 or 5 times slower than using the New operator. So, in general, CreateObject should be avoided. (See "Multithreaded Visual Basic Applications" later in this chapter for an exception to this rule.)

Global objects

The only difference between global and nonglobal SingleUse and MultiUse objects is that you can omit a declaration of a global object when referring to its methods or properties. Let me explain this with an example.

Let's say that you have an object that includes methods for doing math calculations, such as

' In the Math class of the VBLibrary project
Function Max(x As Double, y As Double) As Double
    If x > y Then Max = x Else Max = y
End Function

If you make this class GlobalMultiUse or GlobalSingleUse, you can reference the Max function from within a Visual Basic client application without explicitly creating an object variable that points to an instance of the class:

' In the client application
Print Max(10, 20)                    ' This works!

In other words, you can create a class that exposes Sub and Function methods, and you can see them from within your clients as if the methods were commands and functions, respectively. This is a great convenience because it makes the library a sort of extension of the Visual Basic language. You aren't limited to methods because your class can expose properties, and its clients see the properties as if they were variables. For example, you can add the p constant to Visual Basic:

' A read-only property in the VB2TheMax.Library class
Property Get Pi() As Double
    Pi = 3.14159265358979
End Property

' In the client program
Circumference = Diameter * Pi

You should, however, be aware of the following important detail. Even if you can skip declaring a variable that points to a global object and still access its properties and methods, the omission of this step is just a syntactical convenience that Visual Basic offers you. Behind the scenes, in fact, the language creates a hidden object variable of the proper type and uses that variable each time it invokes one of the class's members. This means that using a global object won't speed up your code at all. On the contrary: The hidden reference is implemented as an auto-instancing variable, so a little overhead accrues when your code accesses its methods and properties because Visual Basic has to decide whether a new instance should be instantiated.

Moreover, since you have no control over this hidden variable you can't even set it to Nothing, so the object it points to will be destroyed only when the application ends. This detail is usually irrelevant but can become meaningful if the object takes a lot of memory and resources.

Interestingly, you might have used Global objects for years without knowing it. In fact, the VBA library is nothing but a collection of global objects; you can explore the VBA library using the Object Browser, and you'll see a number of modules named Math, Strings, and so on. Each of these modules exposes several methods. Because each module is marked as Global, you can use those methods inside Visual Basic applications as if they were native functions. Similarly, the Visual Basic library (labeled VB in the Object Browser) includes a Global module, which exposes the global objects supported by the language, such as App, Printer, and Clipboard. For more information, see "Subclassing the VBA Language" in Chapter 7.

Because global objects are typically used to create libraries of functions, they're often implemented as in-process ActiveX components. On the companion CD, you'll find a nontrivial example of this concept, the VB2TheMax component, which includes 17 classes and over 170 methods that extend the Visual Basic language with many math, date, time, string, and file functions and commands.

Here are two more important details about global objects you need to know. First, such objects are global only outside the component: Inside the component's project, they're regular objects that must be declared and instantiated as usual. Second, as of the time of this writing, Visual Basic is the only development environment that creates clients supporting global objects. You can use your library of global objects with other COM-compliant languages, but in those other languages your global objects are considered to be regular SingleUse or MultiUse objects.

Passing Data Between Applications

The beauty of COM is that components and their clients can pass information back and forth without your having to worry about all the nitty-gritty details of the communication. You can surely write better programs, however, if you understand a bit of what COM does for you behind the scenes.

Marshaling

Marshaling is the operation that COM executes each time data has to be passed from a client to an out-of-process server and back. Marshaling is a complex procedure: Because ActiveX EXE servers and their clients reside in different address spaces, the variables stored in the client's address space aren't immediately visible to the component, and vice versa. Consider what happens when the client executes these statements:

Dim x As New MyServer.MyClass, value As Long
value = 1234
x.MyMethod value

When you pass a variable by reference, the called procedure receives the address of the variable. This address is then used to retrieve and possibly modify the variable's value. When the call originates in another process, however, the called procedure won't be able to access the actual variable's value because the variable is located in another address space and the address received would be meaningless in the context of the procedure. But you know that passing a value to an out-of-process server does work, and it works thanks to COM marshaling. Describing exactly how marshaling works is outside the scope of this book, but the following explanation should suffice for our purposes. (See Figure 16-6.)

Figure 16-6. How COM marshaling works.

  1. When a client application creates an object exposed by an ActiveX EXE server component, COM creates a special proxy module in the client's address space. All calls to the object are redirected to this proxy module, which has the same interface as the original object, with all its methods and properties. As far as the client is concerned, the proxy module is the object.
  2. When the proxy module receives a call from the client application, it finds all the arguments on the stack, so it can easily retrieve their values. Variables passed by reference are no problem because the proxy module is in client's address space, so it can access all client variables.
  3. The proxy module packs all the values received from the client and sends them to a stub module, which is located in the server's address space. The stub module unpacks all the data, retrieves the values of all arguments, and then calls the method in the server's code. As far as the server is concerned, it's being called by the client, not by the stub module. The actual mechanism used for sending data to another process is a complex one, and I won't describe its details here. Let's say that it's one of the magic tricks that COM does for you.
  4. When the method completes its execution, the control is returned to the stub module. If there are values that must be passed back to the client (for example, the return value of a function or an argument passed by reference), the stub packs them and send them back to the proxy module.
  5. Finally, the proxy module unpacks the data received by the stub module and passes the control back to the client's code.

Marshaling is necessary only when you're working with ActiveX EXE components. Because in-process components execute in the client's address space, they can directly access and modify all the client's variables. This explains why ActiveX DLL components are so much faster than out-of-process components.

The marshaling mechanism is quite sophisticated. For example, if a value is passed by reference, the stub creates a temporary variable in the server's address space and passes the address of this variable to the method. The code in the method can therefore read and modify this value. When the method returns, the stub module reads the new value of the variable, packs it, and sends it back to the proxy module, which in turn stores this value at the original variable's memory location.

In addition to allowing the exchange of data, the marshaling mechanism promotes the concept of location transparency, which is essential in the component world. The client code doesn't have to know where the server is located, and at the same time the server doesn't know from what place it's being called. In fact, the same method in the component can be called from outside or inside the component itself, and it will work in the same way in both cases.

The location transparency concept is important because it ensures that the component continues to work even when it's deployed remotely on another machine in the network. In that case, the communication between the proxy and the stub modules is even slower and more complex because it has to rely on the RPC (Remote Procedure Call) protocol to work across machines. But COM takes care of all this. Your client and your server applications will continue to work as before.

Simple data types

To correctly marshal data back and forth, it's mandatory that COM know the format in which the data is stored. Take Visual Basic strings, for example: When the client passes a string to a method, it's really passing a 32-bit pointer to the actual data. The proxy method knows that it's receiving a string and can therefore peek into the client's address space to retrieve the actual characters.

All Visual Basic simple data types are compatible with COM in the sense that COM knows how to marshal them. This means that a server can pass back to its client any numeric, string, or Variant value. Starting with Visual Basic 6, a server can directly return arrays of any type as well. (Servers written with previous versions of Visual Basic could only return arrays stored in Variants.)

Components compiled with Visual Basic 4 or 5 weren't able to pass back UDTs to their clients. Visual Basic 6 does permit components to pass a UDT, provided that the UDT is defined in a Public class and that you have installed DCOM98 or the Service Pack 4 for Windows NT 4. DCOM98 is automatically installed with Windows 98. Even though Windows 2000 hasn't been released as of this writing, it's reasonable to expect that it will support this feature without having to install a service pack.

Don't forget that DCOM98 or the Service Pack 4 must also be installed on your customers' machines. If it isn't, Visual Basic raises the run-time error 458, "Variable uses an Automation Type not supported in Visual Basic." You should trap this error and display a more meaningful message to your users, suggesting that they should upgrade their operating system to support this feature.

Because the UDT must be defined in a Public class, you can't pass UDTs defined in the client application to the server unless the client is an ActiveX EXE program itself. Finally, note that DCOM98 or the Service Pack 4 is required only when your component is passing a UDT to an out-of-process process server. When you're working with ActiveX DLL components, no marshaling takes place and UDTs can be passed back to the client even on plain Windows 95 or Windows NT 4 systems.

Private and Public objects

A server and a client can pass to each other any Public object. This includes both objects defined in the server and objects exposed by other external libraries, such as the Microsoft Word or Microsoft Excel object libraries.

I'll touch on a few more details concerning the marshaling of objects. In addition to the objects defined by class modules in the project, a Visual Basic application deals with objects exposed by three libraries: the Visual Basic, VBA, and VBRUN libraries. These three libraries can deceive you by seeming similar, but they aren't alike, at least for what concerns the visibility of objects.

All the objects exposed by the Visual Basic library (for example, the Form object, the App object, and all the intrinsic controls) are private to that library and so can't be passed to another application, even if that other application is written in Visual Basic. For example, if a Public class in your server includes the following code

' This function can't appear in a Public class module.
Function CurrentForm() As Form
    Set CurrentForm = Form1
End Function

the compiler will refuse to run the application. Conversely, the objects exposed by the VBA and VBRUN libraries are Public and so can be freely passed between different processes. These include the ErrObject and Collection objects (in the VBA library).

Many programmers find the inability to pass ordinary objects, such as forms and controls, between the server and the client a serious limitation and often look for a way to work around it. Such a workaround actually exists; just declare the argument or the return value of the method using As Object or As Variant instead of the actual specific type:

' In the MyClass public module of the MyServer ActiveX EXE project
' Assumes that the project contains a Form1 form and a Text1 text box on it
Function CurrentField() As Object
    Set CurrentField = Form1.Text1
End Function 

' In the client project
Dim x As New MyServer.MyClass
Dim txt As Object
Set txt = x.CurrentField
txt.Text = "This string comes from the client"

The client application declares a generic As Object variable to receive the result of the CurrentField method, which means that you're doing late binding. As you know, late binding is less efficient and prevents you from using the WithEvents keyword.

Things are slightly better with in-process ActiveX servers, which let the client application declare objects using specific object variables. But you should be aware that DLLs created in this way might not work correctly under certain circumstances, so sticking to As Objects variables is usually advisable even when you're working with in-process components. And don't forget that you can use this method only if the client is itself written in Visual Basic.

Now that I've shown you the workaround, let me add that Microsoft explicitly discourages this technique and has warned that it might not work in future versions of Visual Basic. So you use this workaround at your own risk.

This problem raises an interesting question, though: How can the client application access forms and controls hosted in the server application? The answer is that a client should never directly access a private object in the server because that would break the component's encapsulation. If the client needs to manipulate a server's private object, the server should implement a number of methods and properties that provide the required capabilities, for example:

Property Get CurrentFieldText() As String
    CurrentFieldText = Form1.Text1.Text
End Function 
Property Let CurrentFieldText(newValue As String)
    Form1.Text1.Text = newValue
End Property

Notice that Friend methods and properties don't appear in the Public interface of a component and therefore can't be called from outside the current project. For this reason, they never require marshaling, and you can always pass a Private object or a UDT as an argument or the return type of a Friend member.

NOTE
Don't forget that when you marshal an object, you're actually passing a reference, not the object itself. While the client can invoke all the properties and methods of this object, the actual code for these properties and methods runs in the component. This distinction is important especially when you're working with remote components because each time the client uses the object variable, a round-trip to the remote component takes place.

Type libraries

You might wonder how COM can create proxy and stub modules for letting the client communicate with the server. The answer is in the type library, which gathers all the information about the Public classes exposed by the component, including the syntax of individual methods, properties, and events. The type library is usually stored in a file with the extension TLB or OLB, but it can also be embedded in the same EXE, DLL, or OCX file that hosts the component itself. For example, the type library of a component authored with Visual Basic is stored in the component's EXE or DLL file.

If a component has a type library, you can select it in the References dialog box and then explore it using the Object Browser. The References dialog box lists all the type libraries that have been registered in the Registry. If you have a type library that hasn't been registered yet, you can add it to the References dialog box by clicking on the Browse button.

In general, you can use an object without first adding its library to the References dialog box, but you're forced to create it using the CreateObject function and to reference it only through generic As Object variables. Without a type library, in fact, Visual Basic hasn't enough information to let you declare a specific object variable, so you're stuck with late binding. To use specific variables (and therefore early binding), the New keyword, and IntelliSense, you have to add the server's type library to the list of references.

TIP
Visual Basic can create stand-alone type libraries, but you need the Enterprise Edition to do so. The trick is simple: In the Component tab of the Project Properties dialog box, tick the Remote Server Files check box, and then recompile the project. Visual Basic produces a TLB file with the same base name as the project's EXE file.

Performance tips

Now that you know how data is marshaled between the client and the server, you can understand a number of handy techniques that let you improve the performance of your ActiveX EXE components.

A very effective trick that you should always use is to declare methods arguments using ByVal rather than ByRef (unless the routine actually modifies the value and you want it to be returned to the client). Arguments passed by value are never marshaled back to the client because COM knows that they can't change during the call. The ideal situation is when you call a Sub procedure and all arguments are declared using ByVal because in this case no data needs to be marshaled back to the client. You're likely to experience the best improvement when passing long strings. For example, I found that passing a string of 1,000 characters using ByVal is about 20 percent faster than using ByRef.

Cross-process calls are inherently slow. Calling a method with four arguments is almost four times slower than setting four properties. For this reason, your servers should expose methods that let clients quickly set and retrieve properties. For example, let's say that your server exposes the Name, Address, City, and State properties. Besides providing the usual Property procedure pairs, you might write the following GetProperties and SetProperties methods:

' In the MyClass module of the MyServer project
Public Name As String, Address As String
Public City As String, State As String

Sub SetProperties(Optional Name As String, Optional Address As String, _
    Optional City As String, Optional State As String)
    If Not IsMissing(Name) Then Me.Name = Name
    If Not IsMissing(Address) Then Me.Address = Address
    If Not IsMissing(City) Then Me.City = City
    If Not IsMissing(State) Then Me.State = State
End Sub
Sub GetProperties(Optional Name As String, Optional Address As String, _
    Optional City As String, Optional State As String)
    If Not IsMissing(Name) Then Name = Me.Name
    If Not IsMissing(Address) Then Address = Me.Address
    If Not IsMissing(City) Then City = Me.City
    If Not IsMissing(State) Then State = Me.State
End Sub

The client application can therefore set and retrieve all properties (or a subset of them) in a single statement:

' Set all properties in one statement.
Dim x As New MyServer.MyClass
x.SetProperties "John Smith", "1234 East Road", "Los Angeles", "CA"
' Read just the City and State properties.
Dim city As String, state As String
x.GetProperties city:=city, state:=state

You can greatly improve the readability of your client's code using named arguments, as shown in the preceding code snippet.

Another way to reduce the number of cross-process calls is by passing a larger amount of data in an array. You can use an array of Variants because they enable you to pass values of different types. Of course, both the client and the server must agree on the meaning of data passed in the array. This approach is most effective when you don't know how many items you want to pass to the server. For example, suppose that the server exposes a Public collection class with its usual Add, Remove, Count, and Item methods. You might considerably speed up the application if you provide an AddMulti method that lets the client add more than a single item per call:

' In the MyCollection modules of the MyServer project
Private m_myCollection As New Collection

Sub AddMulti(values As Variant)
    Dim v As Variant
    For Each v In values
        m_myCollection.Add v
    Next
End Sub

Note that the values argument is declared as a Variant instead of as an array of Variants, as you might expect, and that the procedure iterates on its members using a For Each...Next loop. This gives this method unparalleled flexibility because you can pass it nearly anything: an array of Strings, an array of Variants, an array of objects, a Variant that contains an array of Strings, Variants, or objects, even a Collection:

' In the client application
Dim x As New MyServer.MyCollection
' Pass an array of Variants built on the fly.
x.AddMulti Array("First", "Second", "Third")

Similarly, if the client application needs to retrieve all the values stored in the MyCollection module, you can speed up things by implementing a method that returns all the items in the collection as an array of Variants:

Function Items() As Variant()
    Dim i As Long
    ReDim result(1 To m_myCollection.Count) As Variant
    For i = 1 To m_myCollection.Count
        ' Object values require the Set command.
        If IsObject(m_myCollection(i)) Then
            Set result(i) = m_myCollection(i)
        Else
            result(i) = m_myCollection(i)
        End If
    Next
    Items = result
End Function

You can get an idea of how you can streamline the interface of your server to provide better performance by having a look at how the Dictionary object is implemented. (See the "Dictionary Objects" section in Chapter 4.)

Finally, you can pass data back and forth from the client to the component by using a UDT that's declared as Public in the component.

Error Handling

An important part of COM programming has to do with error handling. Dealing with errors is always important, of course, but when you're working with ActiveX components you must correctly account for all unanticipated errors.

Error handling in the server component

Errors raised in a component behave exactly like errors that occur in a regular program; if the current procedure isn't protected by an active error handler, the procedure is exited immediately and the control is returned to the caller. If the caller has no active error handler, the control is returned to its caller, and so on until the application encounters a calling procedure with an active error handler or until there's no calling procedure (that is, the topmost procedure was reached and the error was not caught). In this latter case, the error is fatal and the application is terminated.

Properties and methods in an ActiveX component always have a caller—namely the client application—so in a sense all the code inside a procedure is always protected from fatal errors because all errors are returned to the client. The exception to this rule is that event procedures have no direct callers, so you should ensure that nothing can go wrong inside Class_Initialize and Class_Terminate event procedures.

Even if errors in methods and procedures are returned to the client, a well-behaved programmer might want to process them first. Basically, you can follow one of three strategies:

When returning custom errors to the client, you can decide to stick to the COM guidelines for dealing with them. According to such guidelines, all custom errors should be in the range of 512 through 65535 so as not to be confused with COM's own errors, and should be added to the hexadecimal value &H80040000 (or 2,147,221,504). Visual Basic defines a symbolic constant for this value, vbObjectError, so a typical error handler inside an ActiveX server might resemble the following code:

Function Evaluate() As Double
    On Error GoTo ErrorHandler
    ' Open an initialization file (omitted).
    ' ...
    ' Evaluate the result. (This is just a sample expression.)
    Evaluate = a * b(i) / c
    Exit Function
ErrorHandler:
    Select Case Err
        Case 6, 11                ' Overflow or division-by-zero error
            Err.Raise Err.Number  ' can be returned to clients as is.
        Case 53
            Err.Raise 1001 + vbObjectError, , _
                "Unable to load initialization data"
        Case Else
            ' It's always good to provide a generic error code.
            Err.Raise 1002 + vbObjectError, , "Internal Error"
    End Select
End Function

Whatever strategy you decide to adopt, there's one thing that you absolutely shouldn't do—namely, show a message box. In general, the component should delegate the error to the client and let the client decide whether the user should be informed of what went wrong. Showing a message box from within a component is considered a bad programming practice because it prevents the application from running the component remotely.

Error handling in the client application

A correct error handler in the client application is more important than the handler in the server because in most cases the client has no caller to which it can delegate the error. So all errors must be resolved locally. Even if you're absolutely sure that the code in the server can't raise an error (for example, when you're simply retrieving a property), I strongly advise you to provide an error handler anyway. The reason is that when working with ActiveX components, you also have to account for errors raised by COM itself. The list below describes a few errors that COM can raise.

This list shouldn't be considered exhaustive, and you should always account for other errors in your error handler. In summary, a typical error handler in a client application should account for errors raised by three different sources: the server, COM, and the client itself. Here's a possible error handler for a Visual Basic client:

Private Sub cmdEvaluate()
    Dim x As New MyServer.MyClass, res As Double
    On Error GoTo ErrorHandler
    res = x.Evaluate()
    Exit Function
ErrorHandler:
    Select Case Err
        Case 429    ' ActiveX can't create the component.
            MsgBox "Please reinstall the application", vbCritical
            End
        Case 430    ' Automation error
            MsgBox "Unable to complete the operation at this time. " _
                & "Please try again later.", vbCritical
        Case 462    ' The remote server machine is unavailable.
            MsgBox "Please ensure that the server machine " _
                & "is connected and functioning", vbCritical
        Case 1001 + vbObjectError
            MsgBox "Please copy the file VALUES.DAT in the " _
                & "application directory.", vbCritical
        Case 1002 + vbObjectError
            MsgBox "Unknown error. Please contact the manufacturer.", _
                vbCritical
        Case Else
            ' This might be a standard Visual Basic error or COM error.
            ' Do whatever is more appropriate for your application.
    End Select
End Sub

Component Busy and Component Request Pending

As I mentioned previously, COM serializes all the requests coming from clients so that the server can complete them on a first-come, first-served basis. But in certain cases, COM can't accept the client's request; this is the so-called component busy condition. For example, this could happen when your program is using Microsoft Excel as a server and Excel is currently showing a modal dialog box.

Visual Basic assumes that this is a temporary problem and automatically retries periodically. If the problem persists, after 10 seconds Visual Basic displays the Component Busy dialog box, shown in Figure 16-7. The Switch To button activates the other application and brings its window on top of all other windows so that you can correct the problem. (This option has no effect with ActiveX servers that don't have a user interface.) The Retry button lets you retry the operation for an additional 10 seconds. Finally, if you click on the Cancel button you revoke the request, in which case an error &H80010001 (decimal -2,147,418,111) is returned to the client. This is another error you should account for in your error handler.

Figure 16-7. The Component Busy dialog box.

A different problem occurs when COM has accepted the client's request but the component takes too long to complete it. For example, this could happen when the component is waiting for a query to complete, or when it has displayed a message box and is waiting for the user to close it. This problem produces the component request pending condition, which is rather common in the debugging phase, when the server often stops for an unanticipated error.

Because COM has already accepted the request, Visual Basic doesn't have to resubmit it. But until the method returns, the client application is inactive and can't accept input from the user. After 5 seconds, if the user tries to interact with the client application a dialog box like the one you see in Figure 16-8 appears. This is similar to the Server Busy dialog box, but the Cancel button is disabled because the request can't be revoked.

Figure 16-8. The Component Request Pending dialog box.

A few properties of the Application object affect the behavior and the appearance of these dialog boxes. The App.OLEServerBusyTimeout property is the timeout in milliseconds after which the Component Busy dialog box is shown. (The default is 10,000 milliseconds.) The App.OLEServerBusyMsgText and App.OLEServerBusyMsgTitle properties let you customize the contents and the caption of the dialog box shown to the user. If you assign a nonempty string to both these properties, the standard Component Busy dialog box is replaced by a regular message box containing just the OK and Cancel buttons. You can ask Visual Basic not to show the Component Busy dialog box by setting the App.OleServerBusyRaiseError property to True. In this case, no message is displayed and an error &H80010001 is immediately returned to the client. (This is the same error raised when the user clicks on the Cancel button in a Component Busy dialog box.)

A set of similar properties lets you customize Component Request Pending dialog boxes: App.OleRequestPendingTimeout (the default value is 5,000—that is, 5 seconds), App.OleRequestPendingMsgText, and App.OleRequestPendingMsgTitle.

Customizing the Component Busy and Component Request Pending dialog boxes is especially important when your application is dealing with remote components. The default timeouts are often insufficient, so the dialog box is quite likely to appear. When working with remote components, the Switch To button has no effect, so you should provide an alternate message that explains to your users what's happening.

Components with User Interfaces

One of the key advantages of ActiveX EXE servers is that the user can launch them as if they were standard Windows applications. This adds a lot of flexibility but creates a few problems as well.

Determining the StartMode

For example, the program must determine whether it's being run by the user or by the COM subsystem. In the former case, it should display a user interface, which it can do by loading the application's main form. An ActiveX EXE component can distinguish between the two conditions it might be in by querying the App object's StartMode property in the Sub Main procedure:

Sub Main
    If App.StartMode = vbSModeStandalone Then
        ' Being launched as a stand-alone program
        frmMain.Show 
    Else  ' StartMode = vbSModeAutomation
        ' Being launched as a COM component
    End If
End Sub

For the previous code to work, you should set Sub Main as the Startup Object in the General tab of the Project Properties dialog box. A word of caution: When the server is started by COM, Visual Basic executes the Sub Main procedure and instantiates the object, and then COM returns the object to the client application. If the code in the Sub Main procedure or in the Class_Initialize event procedure takes too long, the call could fail with a timeout error. For this reason, you should never execute lengthy operations in these procedures, such as querying a database.

Showing forms

An ActiveX EXE component can display one or more forms as if it were a regular application. For example, the component might be a database browser that can work both as a stand-alone program or as a component to be invoked from other applications.

When the program is working as a COM component (App.StartMode = vbSModeAutomation), however, the client is the foreground application and its windows are likely to cover the server's forms. This raises a problem, and unfortunately Visual Basic has no means of ensuring that a given form becomes the topmost window in the system. For example, the Form object's ZOrder method brings a form in front of all other forms in the same application but not necessarily in front of windows belonging to other applications. The solution to this problem is a call to the SetForegroundWindow API function:

' In the server application
Private Declare Function SetForegroundWindow Lib "user32" _
    (ByVal hwnd As Long) As Long
' A method that displays a modal window
Sub DisplayDialog()
    frmDialog.Show
    SetForegroundWindow frmDialog.hWnd    
End Sub

Unfortunately, Microsoft changed the way this function works under Windows 98, so the preceding approach might not work on that operating system. A solution to this problem, devised by Karl E. Peterson, appeared in the "Ask the VB Pro" column of the February 1999 issue of Visual Basic Programmer's Journal.

Visual Basic 6 supports the new vbMsgBoxSetForeground flag for the MsgBox command, which ensures that the message box appears on top of all the windows that belong to other applications.

Another issue concerns forms in ActiveX EXE components. You often want the form to behave like a modal form, but because modality doesn't work across process boundaries, the user is always able to activate the client's forms using a mouse click. On the other hand, if the server is showing a modal window, the method invoked by the client hasn't returned yet, and the client is therefore unable to react to clicks on its windows. The result is that after a 5-second timeout, a Component Request Pending dialog box appears, explaining that the operation can't be completed because the component isn't responding. (Which is rather misleading, since it's the client that isn't responding, not the server.)

The simplest way to solve this problem is to disable all the forms in the client application before calling the component's method that displays a modal form. This can be done quite easily, thanks to the Forms collection:

Private Sub cmdShowDialogFromComponent_Click()
    SetFormsState False
    x.DisplayDialog
    SetFormsState True
End Sub

' The same routine can disable and reenable all forms.
Sub SetFormsState(state As Boolean)
    Dim frm As Form
    For Each frm In Forms
        frm.Enabled = state 
    Next
End Sub

Limiting the user's actions

An instance of an ActiveX EXE component can serve an interactive user and a client program at the same time. For example, when the user launches the program and then a client requests an object supplied by that server, the server that's currently running provides the object. The opposite isn't generally true; if a client program has created an object and then the user launches the program, a new, distinct instance of the server is loaded in memory.

When the server displays a form as a result of a request from a client application, the server should prevent the user from closing the form. You enforce this by setting a Public property in the form that tells why the form has been displayed and by adding some code to the QueryUnload event procedure:

' In the frmDialog form module
Public OwnedByClient As Boolean

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
    If UnloadMode = vbFormControlMenu Then
        ' The form is being closed by the user.
        If OwnedByClient Then
            MsgBox "This form can't be closed by the end user"
            Cancel = True
        End If
    End If
End Sub

Of course, you must correctly set the OwnedByClient property before showing the form, as in the following code:

' If the form is being displayed because a client requested it
frmDialog.OwnedByClient = True
frmDialog.Show vbModal

In more complex scenarios, the same form might be used by both the user and one or more client applications. In these situations, you should implement a form's property to act as a counter and tell when it's safe to unload the form.

One last word about a component with a user interface. Such a component is inherently a local component and can't run remotely on another machine, for obvious reasons. This means that you're building a solution that won't be scaled easily. Take this detail into account when deciding whether you should add a user interface to your component. One exception to this rule is when the component displays one or more forms for administrative and debugging purposes exclusively and when these forms aren't modal dialog boxes and therefore don't stop the normal execution flow of calls coming from clients.

Compatibility Issues

We human programmers reason in terms of readable names: Each class has a complete name, in the form of servername.classname. This complete name is called the ProgID. Of course, no programmer would purposely create two different classes with the same ProgID, so it seems that name conflicts should never happen. But COM is meant to manage components written by different programmers, so it's too optimistic to assume that no two programmers would create classes with the same ProgID. For this reason, COM uses special identifiers to label components and each class and interface that they expose.

Such identifiers are called GUIDs (Globally Unique Identifiers), and the algorithm that generates them ensures that no two identical GUIDs will ever be generated by two different machines anywhere in the world. GUIDs are 128-bit numbers, and are usually displayed in a readable form as groups of hexadecimal digits enclosed within curly brackets. For example, this is the GUID that identifies the Excel.Application (Excel 97 version) object:

{00024500-0000-0000-C000-000000000046}

When Visual Basic compiles an ActiveX server, it generates distinct identifiers for each of its classes and the interfaces they expose. A class identifier is called a CLSID and an interface identifier is called an IID, but each is just a regular GUID with a different name. All these GUIDs are stored in the type library that Visual Basic creates for the component and registers in the system Registry. The type library is itself assigned another unique identifier.

The role of the Registry

A good COM programmer should have at least a general understanding of the Registry, how COM components are registered, and what happens when a client instantiates a component.

To run a component, Visual Basic has to convert the ProgID of the component's class into its actual CLSID. To do so, it calls a function in the COM run-time library that searches the ProgID in the HKEY_CLASS_ROOT subtree of the Registry. If the search is successful, the CLSID subkey of the found entry contains the identifier of the class. (See Figure 169.) This search is performed at run time when the program instantiates the component using the CreateObject function or at compile time when the component is created using the New operator. Incidentally, this explains why the New operator is slightly faster than CreateObject: When New is used, the executable already contains the CLSID of the class, which saves a time-consuming trip to the Registry. (You get better performance if you use specific variables instead of generic ones.)

Click to view at full size.

Figure 16-9. The RegEdit program shows where COM can find the CLSID of the MyServer.MyClass component.

At this point, COM can search in the HKEY_CLASS_ROOT\CLSID subtree of the Registry for the CLSID of the component. If the component is correctly registered, COM finds all the information it needs under this key. (See Figure 16-10.) In particular, the value of the LocalServer32 key is the path of the EXE file that actually provides the component. Other important information is stored in the TypeLib key, which contains the GUID of the type library. COM uses this GUID for another search in the Registry to learn where the type library is located. (In this particular case, the type library is in the same EXE file that provides the component, but in general it can be stored in a separate file with a .tlb extension.)

Click to view at full size.

Figure 16-10. COM uses the CLSID of the component to retrieve the path of the executable file.

Compatible components

In theory, a careful assessment of your project's requirements would enable you to create a COM component that already includes all the classes and the methods that are necessary for facing the challenges of the real world. In this ideal scenario, you never have to add classes or methods to the component or change its public interface in any way. These components would never raise any compatibility problem: When the Visual Basic compiler converts a method's name into an offset in the VTable, that offset will always be valid and will always point to the same routine in the component.

As you probably suspect, this scenario is too perfect to be true. The reality is that you often need to modify your component, to fix bugs, and to add new capabilities. These changes are likely to cause problems with existing clients. More precisely, if your new version of the component modifies the order in which methods are listed in the VTable, nasty things will happen when an existing client tries to invoke a method at the wrong offset. Similar problems can occur when the same routine expects a different number of arguments or arguments of a different type.

Visual Basic defines three levels of compatibility:

Version Identical The new component has the same project name and the same interfaces as its previous version. For example, this happens if you change the internal implementation of methods and properties but don't modify their names, arguments, and return types. (You can't even add optional arguments to existing methods because that would change the number of values on the stack when the method is invoked.) In this case, Visual Basic compiles the new component using the same CLSIDs and IIDs used in its previous version, and existing clients won't even be aware that the component has actually changed.

Version Compatible If you add new methods and properties but don't modify the interface of existing members, Visual Basic can create a new component that's compatible with its previous version in the sense that all the methods and properties preserve their positions in the VTable. Therefore, existing clients can safely call them. The VTable is extended to account for the new members, which will be used only by the clients that are compiled against the new version. The name of the component's EXE or DLL file can be the same as its previous version, and when you install this component on customers' machines it will overwrite the previous version.

Version Incompatible When you modify the interface of existing methods and properties—for example, by adding or removing arguments (including optional arguments) or by changing their type or the type of the return value—you end up with a component that's incompatible with its previous version. Visual Basic 6 sometimes produces incompatible components even if you change a setting in the Procedure Attributes dialog box. In this case, you must change the name of the EXE or DLL file that hosts the component so that it can coexist with the previous version on your customers' machines. Older client applications can continue to use the previous version of the component, whereas new clients use the new version.

If clients create objects from the component using the New operator, they reference them through their CLSIDs; in this way, no confusion can arise when two different (incompatible) components with the same ProgID are installed on the same machine. But it's preferable that different versions of the component also have distinct ProgIDs, which you accomplish by changing the project name of the newer version.

Let's consider what actually happens when you create a version-compatible component. You might believe that Visual Basic simply creates a new component that inherits the CLSIDs and IIDs from the previous version of the component, but that's not what happens. Instead, Visual Basic generates new identifiers for all the classes and the interfaces in the component. This conforms to COM guidelines, which state that once you publish an interface you should never change it.

The new component, however, also contains information about all the CLSIDs and IIDs of its previous version so that clients that were built for that older version can continue to work as before. When an old client requests an object from the newer component, COM searches the old CLSID in the Registry, which still references the same EXE file. You need to understand how this mechanism works because it explains why a version-compatible component accumulates multiple sets of CLSIDs and IIDs in the executable file and also tends to fill the Registry (both your customers' and your own) with many entries.

Version compatibility in the Visual Basic environment

You know enough to fully understand how you can create compatible components and when you should do it. The Visual Basic environment doesn't permit you to select the CLSIDs of your classes as other languages do; all class and interface identifiers are automatically generated for you. But you can decide whether a new version of the component should preserve the CLSIDs generated for a previous version. Visual Basic offers three settings that affect how identifiers are generated, as you can see in Figure 16-11.

Figure 16-11. Version compatibility settings in the Visual Basic IDE.

No Compatibility Each time you run the project in the environment (or compile it to disk), Visual Basic discards all existing identifiers and regenerates them. This includes all the classes, the interfaces, and the component's type library. This means that clients that worked with previous versions of the component won't work with the new one.

Project Compatibility When you select this mode, you must also select a VBP, EXE, or DLL file with which you must preserve compatibility. In this case, Visual Basic discards all the identifiers for classes and interfaces but preserves the GUID of the component's type library. This setting is useful during the development process because a client application loaded in another instance of Visual Basic won't lose the reference to the server's type library in the References dialog box. When the Visual Basic environment loses a reference to a type library, the corresponding entry in the References dialog box is preceded by a MISSING: label. When this happens, you need to deselect it, close the dialog box, reopen the dialog box, and select the new reference with the same name that has been added.

Of course, just retaining the type library's GUID isn't sufficient for existing clients to continue to work with the new version of the component, but this is hardly a problem because during the test phase you haven't released any compiled clients yet. When you create an ActiveX EXE or DLL project, Visual Basic defaults to project compatibility mode.

Binary Compatibility When you set binary compatibility with an existing component, Visual Basic tries to preserve all the identifiers for the component's type library, classes, and interfaces. You should enforce this mode after you've delivered the component (and its client applications) to your customers because it ensures that you can replace the component without also recompiling all the existing clients. You need to provide the path of the executable file that contains the previous version of the component and that Visual Basic will use to retrieve all the identifiers it needs.

CAUTION
A common mistake is to select as the reference file for binary compatibility the same executable file that's the target of the compilation process. If you make this mistake, each time you compile a new version of your component a new set of GUIDs is added to the EXE file. These identifiers are of no use because they come from compilations in the development phase, and they increase the size of the executable file and add new keys to your Registry that will never be used. Moreover, under certain circumstances you can get a compiler error when the target of the compilation and the file used as a reference for binary compatibility coincide.

Instead, you should prepare an initial version of your component with all the classes and methods already in place (even if empty), and then create an executable file and use it as a reference for all subsequent compilations. In this way, you can select the Binary Compatibility mode but avoid the proliferation of GUIDs. Of course, as soon as you release the first public version of your server, it should become the new reference for binary compatibility. Remember to store such EXE or DLL files in a separate directory so that you don't accidentally overwrite them when you compile the project to disk.

When you're in binary compatibility mode, Visual Basic just tries to maintain the compatibility with the compiled component used as a reference. In fact, at some point during the development of the new version of the component, you might purposely or accidentally break the compatibility—for example, by changing the project's name, the name of a class or a method, or the number or the type of a method's arguments. (See the "Version Incompatible" section, earlier in this chapter.) When you later run or compile the project, Visual Basic displays the dialog box shown in Figure 16-12 and gives you three options:

Click to view at full size.

Figure 16-12. The dialog box for specifying how to handle an incompatible component in Binary Compatibility mode.

At times, you might want to purposely break the binary compatibility with previous versions of the component. This is a useful tactic, for example, when you're going to deploy both the client application and all the components it uses and therefore you're sure that no older client exists on the customer's machine. You can break the binary compatibility by manually resetting the compatibility setting to No Compatibility mode and recompiling the application. The component you obtain doesn't include all the GUIDs from its previous version and therefore is smaller and doesn't fill the Registry with keys and values that would never be used anyway.

Design tips

It's nearly impossible to design a nontrivial component so that you never need to break its compatibility with previous versions when the requirements change. But here are a few tips that can help you preserve compatibility.

First, carefully select the best data type for each method or property. Use Longs rather than Integers because the former provide a larger range of values without hurting performance. Similarly, use Double instead of Single arguments. Using Variant arguments also helps to preserve compatibility when your requirements change.

Second, try to anticipate how your methods could be extended. Even if you aren't willing to write the code that implements those additional capabilities, provide all the necessary methods and arguments that might become necessary later. You can use the Optional and ParamArray keywords to make your methods flexible without affecting the simplicity of existing clients.

Another trick you can use to help you preserve compatibility with older components is to include a sort of do-everything method that can perform different tasks depending on what you pass to it. Such a method might be implemented as follows:

Function Execute(Action As String, Optional Args As Variant) As Variant
    ' No code is here in the initial version of the component.
End Function

Any time you want to add more intelligence to your class but you don't want to break the compatibility with your existing clients, just add some code inside the Execute method, and then recompile without breaking the binary compatibility. For example, you might add the capability to save and load initialization data from a file:

Function Execute(Action As String, Optional Args As Variant) As Variant
    Select Case Action
        Case "LoadData"    ' LoadData and SaveData are private procedures
            LoadData args  ' defined elsewhere in the project.   
        Case "SaveData"
            SaveData args
    End Select
End Function

The Args parameter is a Variant, so you can even pass multiple arguments to it using an array. For example, you can implement a function that evaluates the number of pieces sold within an interval of dates:

    ' Inside the Evaluate method
    Case "EvalSales"
        ' Check that two arguments have been passed.
        If IsArray(Args) Then
            If UBound(Args) = 1 Then 
                ' The arguments are the start and end date.
                Evaluate = EvalSales(Args(0), Args(1))
                Exit Function
            End If
        End If
        Err.Raise 1003, , "A two-element array is expected"

You could then call the Evaluate method as follows:

' Load initialization data.
obj.Evaluate "LoadData", "c:\MyApp\Settings.Dat"
' Build a 2-element array on the fly, and pass it to the Evaluate method.
SoldPieces = obj.Evaluate("EvalSales", Array(#1/1/98#, Now))

Registering a Component

As I've shown previously, much crucial data about a component is stored in the Registry. This information is physically recorded there when the component undergoes a process called registration. When you run an ActiveX project in the IDE, Visual Basic performs a temporary registration of the component so that COM will call Visual Basic itself when a client requests an object from the interpreted component. When you stop the running project, Visual Basic immediately unregisters the component.

When you install a component on the customer's machine, though, you need to perform a permanent registration. There are three ways to permanently register an ActiveX server:

From time to time, you might want to unregister a component. For example, it's always a good idea to unregister a component before you delete it because you will remove all the component's entries in the Registry. If you keep your Registry clean, you have a more efficient system and reduce the number of unanticipated "ActiveX can't create the component" errors that make many COM programmers so nervous. You can unregister a component in two ways, as shown below.

TIP
You can cut down the time necessary to register a DLL server using the following trick. Open Windows Explorer and navigate to the C:\Windows\SendTo directory (assuming that your operating system is installed in the C:\Windows directory). Then create a shortcut to the Regsvr32.exe file and label it RegisterActiveX DLL, or whatever you prefer. After you've done this, you can easily register any DLL component by right-clicking on it and selecting the Register command from the SendTo menu. To easily unregister a DLL, you can create the following two-line batch file:

C:\VisStudio\Common\Tools\Vb\Regutils\regsvr32 /U %1
Exit

and add a shortcut to it in the SendTo menu. (Remember to use a path that matches your system directory configuration, of course.)

Shutting Down the Server

After you have used an object, you must correctly unload the component when you don't need it any longer. If you neglect to do so, your component will continue to hang around in your system, wasting memory, resources, and CPU time. An out-of-process ActiveX component is correctly unloaded when all of the following conditions are met:

Don't forget that only object variables in client applications keep the component alive. If a component has one or more private variables that point to its own objects, they won't prevent COM from destroying the component when no clients are using its objects.

Meeting the last two conditions for unloading a component requires that you pay attention to what the code in the component actually does. For example, many components use hidden forms to host a Timer control that provides background processing capabilities. Consider this deceptively innocent routine:

' In the MyClass module of the MyServer component
Sub StartBackgroundPrinting()
    frmHidden.Timer1.Enabled = True
End Sub

Such a hidden form is enough to keep the component alive even after all its clients have been terminated, until the user resets the system or explicitly kills the server's process from the Task Manager or another similar utility. What's worse is that the component isn't visible, so you won't notice that it's still running unless you look for it in the list of active processes. Of course, the solution to this problem is to explicitly unload the form in the Terminate event procedure of the class, which is always executed when the client releases all the references to the component:

' In the MyClass module of the MyServer component
Private Sub Class_Terminate()
    Unload frmHidden
End Sub

If the server is executing code—for example, a loop that continuously polls the availability of data from a serial port—you must devise a way to stop it when all references are released. Most of the time, you can solve this problem in the same way you solve the hidden form problem, which is by explicitly stopping the code from within the Terminate event procedure. Some complex servers expose a method, such as Quit or Close, that clients can use to indicate that they don't need the component any longer and are therefore about to set all the references to Nothing. For example, this is the approach used by Microsoft Excel and Microsoft Word. (See the spell checker code sample at the beginning of this chapter.)

One last note: A server must not terminate until all of its clients are done with it. Even if a server exposes a method such as Quit, it should never try to force its own termination. If a server abruptly terminates itself—for example, by using an End statement—all the clients that still have one or more references to it receive an error 440, Automation error. The Quit method should be regarded only as a request to the server to prepare to close itself by unloading all of its forms and stopping any background activity.

Persistence

Visual Basic 6 has added the capacity for creating persistable objects, which are objects whose state can be saved and then restored later. The key to object persistence is the new Persistable class attribute and stand-alone PropertyBag objects. Only public creatable objects can be made persistent, so the Persistable attribute appears in the list of class attributes only if Instancing is MultiUse or SingleUse (or their Global variants).

Saving and restoring state

When you set the Persistable attribute of a public creatable class to 1-Persistable, the class module supports three new internal events: InitProperties, WriteProperties, and ReadProperties. In the InitProperties event, the class is expected to initialize its properties, which often means assigning the object's properties their default values. This event fires immediately after the Initialize event:

' A persistable CPerson class with just two properties
Public Name As String
Public Citizenship As String
' Default values
Const Name_Def = ""              
Const Citizenship_Def = "American"

Private Sub Class_InitProperties()
    Name = Name_Def
    Citizenship = Citizenship_Def
End Sub

The Class_WriteProperties event fires when an object is asked to save its internal status. This event procedure receives a PropertyBag object, a virtual bag that should be filled with the current values of the object's properties. You fill the bag by using the PropertyBag's WriteProperty method, which accepts the name of the property and its current value:

Private Sub Class_WriteProperties(PropBag As PropertyBag)
    PropBag.WriteProperty "Name", Name, Name_Def
    PropBag.WriteProperty "Citizenship", Citizenship, Citizenship_Def
End Sub

Finally, the Class_ReadProperties event fires when the class is asked to restore its previous state. The PropertyBag object passed to the event procedure contains the values of properties that were saved previously, and the object can extract them using the PropertyBag's ReadProperty method:

Private Sub Class_ReadProperties(PropBag As PropertyBag)
    Name = PropBag.ReadProperty("Name", Name_Def)
    Citizenship = PropBag.ReadProperty("Citizenship", Citizenship_Def)
End Sub

The last argument passed to both WriteProperty and ReadProperty methods is the property's default value. This value is used to optimize the resources used by the PropertyBag object: If the value of the property coincides with its default value, the property isn't actually stored in the PropertyBag object. This argument is optional, but if you use it you must use the same value within all three event procedures. For this reason, it's advisable to use a symbolic constant.

The PropertyBag object

To have an object save its state, you must create a stand-alone PropertyBag and pass the persistable object to the PropertyBag's WriteProperty method, as shown in the following code snippet:

' Inside a form module
Dim pers As New CPerson, pb As New PropertyBag

' Initialize a CPerson object.
Private Sub cmdCreate_Click()
    pers.Name = "John Smith"
    pers.Citizenship = "Australian"
End Sub
' Save the CPerson object in a PropertyBag.
Private Sub cmdSave_Click()
    ' This statement fires a WriteProperties event in the CPerson class.
    pb.WriteProperty "APerson", pers
End Sub

If the class's Persistable attribute isn't 1-Persistable, you get an error 330, "Illegal Parameter. Can't write object because it doesn't support persistence" when you try to save or restore an object from that class.

Restoring the object's state is easy, too:

Private Sub cmdRestore_Click()
    ' To prove that persistence works, destroy the object first.
    Set pers = Nothing
    ' The next statement fires a ReadProperties event
    ' in the CPerson class.
    Set pers = pb.ReadProperty("APerson")
End Sub

When you pass objects to the WriteProperty and ReadProperty methods, you don't specify a default value. If you omit the last argument and the PropertyBag doesn't contain a corresponding value, Visual Basic raises an error 327, "Data value named 'property name' not found." This is the symptom of a logical error in your program; typically, you have misspelled the name of the property, or you have specified a default value when you saved the object and have omitted it when restoring its state.

Once you have loaded a PropertyBag object with the values of one or more properties, you can also save those values to disk so that you can restore the object's state in subsequent sessions. You do this using the PropertyBag's Contents property, a Byte array that contains all the information about the values stored in the PropertyBag, as the code below demonstrates.

' Save the PropertyBag to a binary file.
Dim tmp As Variant
Open App.Path & "\Propbag.dat" For Binary As #1
tmp = pb.Contents
Put #1, , tmp
Close #1

The previous routine uses a temporary Variant variable to simplify the saving of the Byte array. You can use the same trick when it's time to reload the contents of the file:

' Reload the PropertyBag object from file.
Dim tmp As Variant
Set pb = New PropertyBag
Open App.Path & "\Propbag.dat" For Binary As #1
Get #1, , tmp
pb.Contents = tmp
Close #1

CAUTION
If you're testing the application in the IDE, you might find that you're unable to reload the state of an object saved to disk in a previous session because of an error 713 "Class not registered." This happens because the property bag embeds the CLSID of the object being saved. By default, each time you rerun the application in the IDE, Visual Basic generates a new CLSID for each class in the project, so it won't be able to reload the state of an object with a different CLSID. To work around this issue, you should enforce the Binary Compatibility mode, as explained in the "Version Compatibility in the Visual Basic Environment" section earlier in this chapter.

Persistent object hierarchies

The persistence mechanism can also work with object hierarchies; each object in the hierarchy is responsible for saving its dependent objects in its WriteProperties event procedure and restoring them in its ReadProperties procedure. Everything works as long as all the objects in the hierarchy have their Persistable attribute set to 1Persistable. For example, you can extend the CPerson class with a Children collection that contains other CPerson objects, and you can account for this new property in the WriteProperties and ReadProperties event procedures:

Public Children As New Collection        ' A new public property

Private Sub Class_WriteProperties(PropBag As PropertyBag)
    Dim i As Long
    PropBag.WriteProperty "Name", Name, Name_Def
    PropBag.WriteProperty "Citizenship", Citizenship, Citizenship_Def
    ' First, save the number of children (default = 0).
    PropBag.WriteProperty "ChildrenCount", Children.Count, 0
    ' Next, save all the children one by one.
    For i = 1 To Children.Count
        PropBag.WriteProperty "Child" & i, Children.Item(i)
    Next
End Sub

Private Sub Class_ReadProperties(PropBag As PropertyBag)
    Dim i As Long, ChildrenCount As Long
    Name = PropBag.ReadProperty("Name", Name_Def)
    Citizenship = PropBag.ReadProperty("Citizenship", Citizenship_Def)
    ' First, retrieve the number of children.
    ChildrenCount = PropBag.ReadProperty("ChildrenCount", 0)
    ' Next, restore all the children, one by one.
    For i = 1 To ChildrenCount
        Children.Add PropBag.ReadProperty("Child" & i)
    Next
End Sub

Interestingly, the resulting PropertyBag object contains many properties labeled Name, Citizenship, Child1, Child2, and so on, but this isn't a problem because they are encapsulated in a hierarchy of properties so that no confusion can arise. In other words, the Name value stored in the Child1 subtree is distinct from the Name value stored in the Child2 subtree, and so on. If you want to study this technique further, you can browse the code of the demonstration program on the companion CD.

CAUTION
You need to be sure that the hierarchy doesn't contain any circular references. Or at least you need to be certain that the references are dealt with correctly when you're storing and restoring objects. To explain why this is such an important consideration, suppose that the CPerson class exposes a Spouse property that returns a reference to a person's wife or husband, and then think of what would happen if each object attempts to save the state of this property. Mr. Smith saves the state of Mrs. Smith, who in turn saves the state of Mr. Smith, who in turn saves the state of Mrs. Smith...and so on, until you get an "out of stack space" error.

Depending on the nature of the relationship, you must devise a different strategy to avoid being caught in such endless loops. For example, you could decide that you'll save just the Name of a person's consort instead of its entire state, but then you have to correctly rebuild the relationship in the ReadProperties event procedure.

Using the PropertyBag with any class module

I've explained that the Persistable property is available only if the class is Public and creatable. In a sense, this is a requirement of COM, not of Visual Basic. This doesn't mean, however, that you can't take advantage of the PropertyBag object—and its capability to store data in all the Automation-compliant formats—to implement a sort of object persistence. In fact, the only thing you can't really do is implement custom class events, such as WriteProperties and ReadProperties. But you can add a special property of the class that sets and returns the current state of the object and uses a private PropertyBag object for the low-level implementation of the serialization mechanism. In the following example, I have a CPerson class module that exposes a special property called ObjectState:

' The CPerson class module
Public FirstName As String, LastName As String

Property Get ObjectState() As Byte()
    Dim pb As New PropertyBag
    ' Serialize all the properties into the PropertyBag.
    pb.WriteProperty "FirstName", FirstName, ""
    pb.WriteProperty "LastName", LastName, ""
    ' Return the PropertyBag's Contents property.
    ObjectState = pb.Contents
End Property

Property Let ObjectState(NewValue() As Byte)
    Dim pb As New PropertyBag
    ' Create a new PropertyBag with these contents.
    pb.Contents = NewValue()
    ' Deserialize the class's properties.
    FirstName = pb.ReadProperty("FirstName", "")
    LastName = pb.ReadProperty("LastName", "")
End Property

When implementing this form of persistence, the code in the client application is slightly different:

Dim p As New CPerson, state() As Byte

p.FirstName = "Francesco"
p.LastName = "Balena"
' Save the state into a Byte array.
state() = p.ObjectState
' ...
' Create a new object, and restore its state from the Byte array.
Dim p2 As New CPerson
p2.ObjectState = state()
Print p2.FirstName & " " & p2.LastName       ' Displays "Francesco Balena".

Of course, if the object has dependent objects, they must expose the ObjectState property as well so that the main object can correctly serialize the state of its child objects. A cleaner approach would be to define the IObjectState interface and have this interface be implemented by all the classes that you want to make persistent. Notice that this technique works because the object being deserialized is created by the component's code, not by the PropertyBag object, so there's no restriction about its Instancing property. This technique also works inside Standard EXE programs and is actually one of the most useful unknown tricks that you can perform with the PropertyBag object.

Persistent ADO Recordsets

One fact that you won't find in the Visual Basic documentation is that under certain conditions you can even pass an ADO Recordset to a PropertyBag object. More precisely, any ADO Recordset that can be saved to a file using its Save method—for example, a Recordset with CursorLocation set to adUseClient—can be also be passed to the WriteProperty method of a PropertyBag. This gives you unparalleled flexibility in exchanging data among your applications. For example, instead of saving the contents of one single Recordset to a file using a Save method, you can store multiple related Recordsets inside one PropertyBag object, and then save its Contents property to file.